﻿//===================================================================================
// Copyright (c) Ryan Garrett.  All rights reserved.
// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY
// OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT
// LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS FOR A PARTICULAR PURPOSE.
//===================================================================================

//WRITE: Remember in the guidance to point out that the events are only for updating the views. To react to changes, use the overrides. Not the event handlers
//WRITE: Make sure that in the guidance you mention that in the case of a clean/null VM assignment with a committed dirty/dirtywithin VM, always reset the committed VM first. Otherwise you could get a double flicker of change notification flags
//WRITE: Make sure to write about how to identify potential double flicker problems with validation
//WRITE: Remember to emphasize that during the de-serialization phase the VMs must be created the same as before. Meaning the same properties, etc.
//FUTURE: If a model property does not support the change notification behavior, make sure it ignores event registrations of would be observers
//FUTURE: Look at rehydration with respect to AssignValueAsCommitted and directly assigning the Value property. Write about different ways rehydration can be handled
//TODO: Test IsComputed and IsNullable
//TODO: Test ModelPropertyType

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Caffeine.Core.Metadata;
using Caffeine.Core.ModelProperty;

namespace Caffeine.Core
{
    /// <summary>
    /// Delegate for validating a model property
    /// </summary>
    /// <typeparam name="T">Type of child model</typeparam>
    /// <param name="propertyToValidate">Property to validate</param>
    /// <param name="validationErrors">Errors found during validation</param>
    /// <param name="isValid">Validation result</param>
    public delegate void PropertyValidationDel<T>
    (
        ModelProperty<T> propertyToValidate, 
        ref IEnumerable<ValidationError> validationErrors, 
        ref Boolean isValid
    );

    /// <summary>
    /// Designed to exist as a property of a ModelBase subtype. It provides property level change
    /// notification and validation. Its default state is committed and valid (i.e. IsDirty == false and IsInvalid == false, respectively).
    /// </summary>
    /// <typeparam name="T">The type of value being monitored for the parent model</typeparam>
    public abstract class ModelProperty<T> : ModelScalarPropertyBase, INotifyPropertyChanged
    {
        #region State

        private IPropertySync<T> _propertySync;

        #endregion

        #region Constructors

        [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors")]
        internal ModelProperty(ModelBase parent, String name, Nullable<ModelPropertyBehaviors> behavior, Boolean isWrapped)
            : base(parent, name, behavior, isWrapped)
        {
            InternalValue = GetDefaultValue();
            InternalCommittedValue = InternalValue;
            AllowSync = true;
        }

        #endregion

        #region Public

        /// <summary>
        /// Gets or sets the value for this property. This property supports INotifyPropertyChanged.
        /// </summary>
        public T Value
        {
            get
            {
                return InternalValue;
            }
            set
            {
                SetValue(value, true);
            }
        }

        /// <summary>
        /// Gets or sets the last committed value of this property. This property supports INotifyPropertyChanged.
        /// </summary>
        public T CommittedValue
        {
            get
            {
                return InternalCommittedValue;
            }
            private set
            {
                if (!Compare(InternalCommittedValue, value))
                {
                    InternalCommittedValue = value;
                    RaiseNotifyPropertyChanged("CommittedValue");
                }
            }
        }

        /// <summary>
        /// Gets the type of value contained by this property
        /// </summary>
        public override Type PropertyType
        {
            get
            {
                return typeof(T);
            }
        }

        /// <summary>
        /// Gets or sets a delegate for comparing values assigned to this property. Null by default. 
        /// </summary>
        public Func<T, T, Boolean> CompareMethod
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets a delegate used for validating this property
        /// </summary>
        public PropertyValidationDel<T> ValidationMethod
        {
            get;
            set;
        }

        /// <summary>
        /// Gets or sets a property sync interface. If this is non-null, this property will sync its value to destination property
        /// </summary>
        public IPropertySync<T> PropertySync
        {
            get
            {
                return _propertySync;
            }
            set
            {
                if (_propertySync != value)
                {
                    if (IsModelProperty)
                    {
                        throw new InvalidOperationException("Cannot synchronize properties that contain models");
                    }

                    _propertySync = value;
                }
            }
        }

        /// <summary>
        /// Gets whether or not this is a computed property
        /// </summary>
        public virtual Boolean IsComputed
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Gets whether or not this is a nullable value type property
        /// </summary>
        public virtual Boolean IsNullable
        {
            get
            {
                return false;
            }
        }

        /// <summary>
        /// Assigns a value to this property that is considered committed immediately
        /// </summary>
        /// <param name="value">Value to assigne</param>
        public void AssignCommitted(T value)
        {
            if (IsDirty)
            {
                throw new InvalidOperationException("AssignCommitted cannot be called when property is dirty");
            }

            SetValue(value, false);
            Commit();
        }

        /// <summary>
        /// Gets the default value for this property
        /// </summary>
        /// <returns>The default value for this property</returns>
        public virtual T GetDefaultValue()
        {
            return default(T);
        }

        /// <summary>
        /// Assigns this property to its default value
        /// </summary>
        public override void AssignDefaultValue()
        {
            Value = GetDefaultValue();
        }

        /// <summary>
        /// Allows you to compare two values against each other the same way this property will
        /// </summary>
        /// <param name="currentValue">Current value</param>
        /// <param name="newValue">New value</param>
        /// <returns>Whether or not this property considers them equal</returns>
        public virtual Boolean Compare(T currentValue, T newValue)
        {
            Boolean areEqual;

            if (CompareMethod == null)
            {
                areEqual = OnCompareValues(currentValue, newValue);
            }
            else
            {
                areEqual = CompareMethod(currentValue, newValue);
            }

            return areEqual;
        }

        /// <summary>
        /// If this property's PropertySync is non-null, this call will force it to update its destination properties which
        /// it is synced to. If the destination property specified only updates on commits, you must use the ForceSync(true)
        /// overload to force values to be synced.
        /// </summary>
        public void ForceSync()
        {
            ForceSync(false);
        }

        /// <summary>
        /// If this property's PropertySync is non-null, this call will force it to update its destination properties which
        /// it is synced to
        /// </summary>
        /// <param name="signalCommit">
        /// True to sync to a destination property if it only updates on commits. Otherwise, false to update
        /// the destiation property only if it updates on value changes.
        /// </param>
        public void ForceSync(Boolean signalCommit)
        {
            if (PropertySync != null)
            {
                if (!signalCommit)
                {
                    if (!UpdateSyncOnCommit)
                    {
                        PropertySync.SignalValueChange(Value);
                    }
                }
                else //Signaling commit
                {
                    if (UpdateSyncOnCommit)
                    {
                        PropertySync.SignalValueChange(Value);
                    }
                }
            }
        }

        /// <summary>
        /// Commit the value of this property
        /// </summary>
        public override void Commit()
        {
            if (!Compare(CommittedValue, Value))
            {
                OnBeforeCommit();
                CommittedValue = Value;
                OnAfterCommit();
                IsDirty = false;
                Parent.RaiseOnPropertyCommittedValueChanged(this);

                if (PropertySync != null && UpdateSyncOnCommit)
                {
                    PropertySync.SignalValueChange(CommittedValue);
                }
            }
        }

        /// <summary>
        /// Assigns Value to a native type
        /// </summary>
        /// <param name="property">Property to retrieve Value from</param>
        /// <returns>Value property</returns>
        public static implicit operator T(ModelProperty<T> property)
        {
            return property.Value;
        }

        #endregion

        #region Internal

        internal override ModelBase GetModelValue()
        {
            return Value as ModelBase;
        }

        internal override ModelBase GetCommittedModelValue()
        {
            return CommittedValue as ModelBase;
        }

        internal override Object GetValueForTesting()
        {
            return Value;
        }

        internal override Object GetCommittedValueForTesting()
        {
            return CommittedValue;
        }

        internal override Object GetPropertySyncForTesting()
        {
            return PropertySync;
        }

        internal override Object GetCompareMethodForTesting()
        {
            return CompareMethod;
        }

        internal override Object GetValidationMethodForTesting()
        {
            return ValidationMethod;            
        }

        internal override ModelScalarPropertyMetadata GetMetadata()
        {
            return new ModelScalarPropertyMetadata()
            {
                Value = Value,
                CommittedValue = (IsDirty) ? CommittedValue as Object : null,
                UpdateSyncOnCommit = UpdateSyncOnCommit,
                AllowSync = AllowSync,
                IsDefault = IsDefault,
                Name = Name,
                IsDirty = IsDirty,
                IsInvalid = IsInvalid,
                ValidationErrors = GetValidationErrorMetadata()
            };
        }

        #endregion

        #region Protected

        protected T InternalValue
        {
            get;
            set;
        }

        protected T InternalCommittedValue
        {
            get;
            set;
        }

        protected override void OnReset()
        {
            Value = CommittedValue;
        }

        protected abstract Boolean OnCompareValues(T valueX, T valueY);

        protected virtual void ConfigureParentChildRelationships(T previousValue, T newValue)
        {
        }

        protected virtual void OnBeforeCommit()
        {
        }

        protected virtual void OnAfterCommit()
        {
        }

        protected virtual void SetValue(T value, Boolean setIsDirtyFlag)
        {
            if (!Compare(InternalValue, value))
            {
                T previousValue = InternalValue;
                InternalValue = value;
                RaiseNotifyPropertyChanged("Value");

                if (IsModelProperty)
                {
                    ConfigureParentChildRelationships(previousValue, InternalValue);
                }

                IsDefault = Compare(InternalValue, GetDefaultValue());

                if (setIsDirtyFlag && this.CheckSetsIsDirty())
                {
                    IsDirty = !Compare(InternalValue, CommittedValue);
                }

                Validate();

                Parent.RaiseOnPropertyValueChanged(this);

                if (Parent.PropertyChangeCounter != null && this.CheckSupportsPropertyChangeCounting())
                {
                    Parent.PropertyChangeCounter.IncrementChangeCount(this);
                }

                if (PropertySync != null && !UpdateSyncOnCommit)
                {
                    PropertySync.SignalValueChange(value);
                }
            }
        }

        protected override Boolean CheckIsValidationMethodSet()
        {
            return ValidationMethod != null;
        }

        protected override void Validate(ref IEnumerable<ValidationError> validationFailureReasons, ref Boolean isValid)
        {
            validationFailureReasons = null;
            ValidationMethod(this, ref validationFailureReasons, ref isValid);
        }

        protected override void OnApplyScalarPropertyMetadata(ModelScalarPropertyMetadata metadata, IModelFactory factory)
        {
            InternalValue = (T)metadata.Value;

            if (metadata.IsDirty)
            {
                InternalCommittedValue = (T)metadata.CommittedValue;
            }
            else
            {
                InternalCommittedValue = InternalValue;
            }
        }

        #endregion
    }
}
